﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Collections;

using Roslyn.Services;
using Roslyn.Compilers.CSharp;
using Roslyn.Compilers.Common;
using Roslyn.Compilers;

using King.Reflection;
using King.Extensions;
using King.CSharp.Text;
using King.CSharp.Reflection;

namespace King.CSharp {
    
    public sealed class CSharpWorkspace {
        public static CSharpWorkspace LoadSolution(string solutionFileName, string configuration = "Debug", string platform = "AnyCPU") {
            var workspace = Workspace.LoadSolution(solutionFileName, configuration, platform);
            return new CSharpWorkspace(workspace);
        }
        public static CSharpWorkspace LoadProject(string projectFileName, string configuration = "Debug", string platform = "AnyCPU", string language = null) {
            var workspace = Workspace.LoadStandAloneProject(projectFileName, configuration, platform);
            return new CSharpWorkspace(workspace);
        }

        private IWorkspace m_workspace;
        private ISolution m_originalSolution;
        private CSharpSolution m_solution;

        internal CSharpWorkspace(IWorkspace workspace) {
            m_workspace = workspace;
            m_solution = new CSharpSolution(this, workspace.CurrentSolution.Id);
            m_originalSolution = workspace.CurrentSolution;
        }

        internal void UpdateDocument(CSharpDocument document, IText text) {
            m_workspace.UpdateDocument(document.Id, text);
        }

        public bool Save() {
            return m_workspace.ApplyChanges(m_originalSolution, m_workspace.CurrentSolution);
        }
        public CSharpSolution Solution {
            get {
                m_solution.Update(m_workspace.CurrentSolution);
                return m_solution; 
            }
        }

#if false
        // activation
        public static IWorkspace LoadSolution(string solutionFileName, string configuration = "Debug", string platform = "AnyCPU");
        public static IWorkspace LoadStandAloneProject(string projectFileName, string configuration = "Debug", string platform = "AnyCPU", string language = null);

        // solution
        public virtual ISolution CurrentInProgressSolution { get; }
        public virtual ISolution CurrentSolution { get; }

        // commit
        public virtual Task<bool> ApplyChangesAsync(ISolution oldSolution, ISolution newSolution, CancellationToken cancellationToken = null);

        // edit
        public virtual ISolutionEdit CreateEdit(string description);
        public virtual bool EditInProgress { get; }
        public virtual IWorkspaceVersion Version { get; }

        // document
        public virtual Task OpenDocumentAsync(DocumentId documentId);
        public virtual Task UpdateParseOptionsAsync(DocumentId documentId, IParseOptions options);
        public virtual Task UpdateDocumentAsync(DocumentId documentId, IText newText);
        public virtual Task CloseDocumentAsync(DocumentId documentId);
        public virtual Task<DocumentId> AddDocumentAsync(ProjectId projectId, IEnumerable<string> folders, string preferredDocumentName, IText text = null, SourceCodeKind sourceCodeKind = SourceCodeKind.Regular);
        public virtual Task RemoveDocumentAsync(DocumentId documentId);

        // text
        public bool TryGetDocument(IText text, out IDocument document);
        public bool TryGetDocumentFromInProgressSolution(IText text, out IDocument document);
        public virtual DocumentId GetDocumentId(ITextContainer textContainer);
        
        // project
        public virtual Task<ProjectId> AddProjectAsync(string projectName, string language);
        public virtual Task RemoveProjectAsync(ProjectId projectId);
        public virtual Task UpdateCompilationOptionsAsync(ProjectId projectId, ICompilationOptions options);

        // references
        public virtual Task AddMetadataReferenceAsync(ProjectId fromProjectId, MetadataReference toMetadata);
        public virtual Task RemoveMetadataReferenceAsync(ProjectId fromProjectId, MetadataReference toMetadata);
        public virtual Task AddProjectReferenceAsync(ProjectId fromProjectId, ProjectId toProjectId);
        public virtual Task RemoveProjectReferenceAsync(ProjectId fromProjectId, ProjectId toProjectId);

        // listeners
        public void RemoveListener(IWorkspaceListener listener);
        public void AddListener(IWorkspaceListener listener);
        public IEnumerable<IWorkspaceListener> Listeners { get; }
#endif
    }

    internal class CSharpWorkspaceObjects<T> : IEnumerable<T> where T : CSharpWorkspaceObject {
        private Dictionary<object, T> m_objects;
        private Func<IEnumerable<object>> m_getImmutableObjects;

        internal CSharpWorkspaceObjects(Func<IEnumerable<object>> getImmutableObjects) {
            m_getImmutableObjects = getImmutableObjects;
        }

        private object GetId(object o) {
            return o.GetType().GetProperty("Id").GetValue(o);
        }

        private IEnumerable<T> Items() {
            foreach (var immutableObject in m_getImmutableObjects()) {
                var id = immutableObject.GetType().GetProperty("Id").GetValue(immutableObject);
                var o = m_objects[id];
                o.Update(immutableObject);
                yield return o;
            }
        }

        public IEnumerator<T> GetEnumerator() {
            return Items().GetEnumerator();
        }
        IEnumerator IEnumerable.GetEnumerator() {
            return GetEnumerator();
        }
    }

    public abstract class CSharpWorkspaceObject {

        private object m_id;
        private CSharpWorkspace m_workspace;

        internal CSharpWorkspaceObject(CSharpWorkspace workspace, object id) {
            m_id = id;
            m_workspace = workspace;
        }

        internal object Id {
            get { return m_id; }
        }
        internal abstract void Update(object immutableObject);

        public CSharpWorkspace Workspace {
            get { return m_workspace; }
        }

        public override bool Equals(object obj) {
            return m_id.Equals(obj);
        }
        public override int GetHashCode() {
            return m_id.GetHashCode();
        }
    }

    public sealed class CSharpSolution : CSharpWorkspaceObject {

        private ISolution m_solution;
        private Dictionary<ProjectId, CSharpProject> m_projects;

        internal CSharpSolution(CSharpWorkspace workspace, SolutionId id)
            : base(workspace, id) {
        }
        
        internal override void Update(object solution) {
            m_solution = (ISolution)solution;
        }
        internal new SolutionId Id {
            get { return (SolutionId)base.Id; }
        }
        internal CSharpProject GetProject(ProjectId projectId) {
            return m_projects.GetValueOrDefault(projectId);
        }

        public IEnumerable<CSharpProject> Projects() {
            if (m_projects == null)
                m_projects = m_solution.Projects
                    .ToDictionary(o => o.Id, o => new CSharpProject(this, o.Id));

            foreach (var project in m_solution.Projects) {
                var csProject = m_projects[project.Id];
                csProject.Update(project);
                yield return csProject;
            }
        }

#if false
        bool HasProjects { get; }
        SolutionId Id { get; }
        IMetadataFileProvider MetadataFileProvider { get; }
        IEnumerable<ProjectId> ProjectIds { get; }
        IEnumerable<IProject> Projects { get; }

        ISolution AddDocument(DocumentId documentId);
        ISolution AddDocument(IDocument document);
        ISolution AddDocument(DocumentId documentId, string displayName, Func<CancellationToken, IText> loadText);
        ISolution AddDocument(DocumentId documentId, string displayName, IText text);
        ISolution AddMetadataReference(ProjectId projectId, MetadataReference metadataReference);
        ISolution AddMetadataReferences(ProjectId projectId, IEnumerable<MetadataReference> metadataReferences);

        ISolution AddProject(IProject project);
        ISolution AddProject(ProjectId projectId, string language, string assemblyName, string displayName);
        ISolution AddProject(ProjectId projectId, string language, string displayName, Func<string> getAssemblyName, Func<IEnumerable<DocumentInfo>> getDocuments, Func<IEnumerable<ProjectId>> getProjectReferences, Func<IEnumerable<MetadataReference>> getMetadataReferences, Func<ICompilationOptions> getCompilationOptions, Func<IParseOptions> getParseOptions, AssemblyResolver assemblyResolver = null, bool isSubmission = false, ProjectId previousSubmissionProjectId = null);
        
        ISolution AddProjectReference(ProjectId projectId, ProjectId referencedProject);
        ISolution AddProjectReferences(ProjectId projectId, IEnumerable<ProjectId> referencedProjects);
        
        ISolution Clone();
        ISolution CloseDocument(DocumentId documentId, Func<CancellationToken, IText> loadText);

        bool ContainsDocument(DocumentId documentId);
        bool ContainsProject(ProjectId projectId);

        ISolutionDifferences GetDifferences(ISolution oldSolution);

        IDocument GetDocument(CommonSyntaxTree syntaxTree);
        IDocument GetDocument(DocumentId documentId);

        IProject GetProject(ProjectId projectId);
        IProject GetProjectByAssemblyName(string assemblyName);
        IProject GetProjectByDisplayName(string displayName);

        MetadataReference GetReferencedProjectMetadata(ProjectId fromProject, ProjectId toProject, CancellationToken cancellationToken = null);

        ISolution OpenDocument(DocumentId documentId, IText newText);
        ISolution RemoveAllDocuments(ProjectId projectId);
        ISolution RemoveAllMetadataReferences(ProjectId projectId);
        ISolution RemoveAllProjectReferences(ProjectId projectId);

        ISolution RemoveDocument(DocumentId documentId);
        ISolution RemoveMetadataReference(ProjectId projectId, MetadataReference metadataReference);
        ISolution RemoveProject(ProjectId projectId);
        ISolution RemoveProjectReference(ProjectId projectId, ProjectId referencedProject);
        ISolution UpdateCompilationOptions(ProjectId projectId, ICompilationOptions options);
        
        ISolution UpdateDocument(IDocument document);
        ISolution UpdateDocument(DocumentId documentId, IText text);
        ISolution UpdateDocument(DocumentId documentId, IText newText, TextChangeRange[] changes);
        ISolution UpdateFolders(DocumentId documentId, IEnumerable<string> folderNames);
        ISolution UpdateMetadataReference(ProjectId projectId, MetadataReference oldMetadata, MetadataReference newMetadata);
        ISolution UpdateParseOptions(ProjectId projectId, IParseOptions options);
        ISolution UpdateSourceCodeKind(DocumentId documentId, SourceCodeKind kind);
#endif
    }

    public sealed class CSharpProject : CSharpWorkspaceObject {

        private CSharpSolution m_solution;
        private IProject m_project;
        private Dictionary<DocumentId, CSharpDocument> m_documents;
        private CSharpCompilation m_compilation;

        public CSharpProject(CSharpSolution solution, ProjectId id)
            : base(solution.Workspace, id) {

            m_solution = solution;
        }

        internal override void Update(object project) {
            m_project = (IProject)project;
            m_compilation = null;
        }
        internal new ProjectId Id {
            get { return (ProjectId)base.Id; }
        }

        public CSharpSolution Solution {
            get { return m_solution; }
        }
        public string AssemblyName {
            get { return m_project.AssemblyName; }
        }
        public string DisplayName {
            get { return m_project.Name; }
        }

        public IEnumerable<CSharpDocument> Documents() {
            if (m_documents == null)
                m_documents = m_project.Documents
                    .ToDictionary(o => o.Id, o => new CSharpDocument(this, o.Id));

            foreach (var document in m_project.Documents) {
                var csDocument = m_documents[document.Id];
                csDocument.Update(document);
                yield return csDocument;
            }
        }

        //public IEnumerable<CSharpReference> References() {
        //    if (m_references == null) {
        //        var metadataReferences = m_project.MetadataReferences
        //            .Select(o => {
        //                CSharpReference reference;

        //                var fileReference = o as MetadataFileReference;
        //                if (fileReference == null)
        //                    reference = new CSharpFileReference(fileReference.FullPath);
        //                else
        //                    reference =  new CSharpAssemblyReference(o.Display);

        //                return reference;
        //            });

        //        var projectReferences = m_project.ProjectReferences
        //            .Select(o => Solution.GetProject(o))
        //            .Select(o => new CSharpProjectReference(o));

        //        m_references = metadataReferences.Cast<CSharpReference>()
        //            .Union(projectReferences.Cast<CSharpReference>())
        //            .ToArray();
        //    }
        //    return m_references;
        //}
        public CSharpCompilation Compilation {
            get {
                //if (m_compilation == null) {
                //    var compilation = (Compilation)m_project.GetCompilation();
                //    m_compilation = new CSharpCompilation(compilation);
                //}

                //return m_compilation;
                throw new NotImplementedException();
            }
        }
        public CSharpAssembly Assembly {
            get { 
                throw new NotImplementedException();
                //return Compilation.Assembly; 
            }
        }

        public override string ToString() {
            return DisplayName;
        }
#if false
        bool HasDocuments { get; }
        ProjectId Id { get; }
        bool IsSubmission { get; }
        ProjectId PreviousSubmissionProjectId { get; }
        IProjectDifferences GetDifferences(IProject oldProject);

        ISolution Solution { get; }

        string AssemblyName { get; }
        string DisplayName { get; }

        IParseOptions ParseOptions { get; }
        ICompilationOptions CompilationOptions { get; }

        IEnumerable<DocumentId> DocumentIds { get; }
        IEnumerable<IDocument> Documents { get; }
        bool ContainsDocument(DocumentId documentId);
        IDocument GetDocument(CommonSyntaxTree syntaxTree);
        IDocument GetDocument(DocumentId documentId);

        IEnumerable<ProjectId> ProjectReferences { get; }

        IEnumerable<MetadataReference> MetadataReferences { get; }

        AssemblyResolver AssemblyResolver { get; }
        ILanguageServiceProvider LanguageServices { get; }

        ICompilation GetCompilation(CancellationToken cancellationToken = null);
        bool IsEquivalentTo(IProject project, CancellationToken cancellationToken = null);
        bool IsPubliclyEquivalentTo(IProject project, CancellationToken cancellationToken = null);
#endif
    }

    public sealed class CSharpDocument : CSharpWorkspaceObject {

        private CSharpProject m_project;
        private IDocument m_document;
        private CSharpCompilationUnitSyntax m_compilationUnit;

        internal CSharpDocument(CSharpProject project, DocumentId id)
            : base(project.Workspace, id) {

            m_project = project;
        }

        internal override void Update(object immutableObject) {
            m_document = (IDocument)immutableObject;
            m_compilationUnit = null;
        }
        internal new DocumentId Id {
            get { return (DocumentId)base.Id; }
        }

        public CSharpProject Project {
            get { return m_project; }
        }
        public string DisplayName {
            get { return m_document.Name; }
        }
        public IEnumerable<string> Folders() {
            return m_document.Folders;
        }
        public CSharpCompilationUnitSyntax CompilationUnit {
            get {
                if (m_compilationUnit == null)
                    m_compilationUnit = new CSharpCompilationUnitSyntax((SyntaxTree)m_document.GetSyntaxTree(), Project.Compilation);
                return m_compilationUnit;
            }
        }
        public void Transform(
            IEnumerable<CSharpObject> targets,
            Func<CSharpObject, CSharpObject> transform) {

            var newCompilationUnit = CompilationUnit.Transform(targets, transform);
            var text = newCompilationUnit.Tree.GetText(default(CancellationToken));
            Workspace.UpdateDocument(this, text);
        }

        public override string ToString() {
            return Folders().StringJoin(separator: "/", suffix: "/") + DisplayName;
        }
#if false
        DocumentId Id { get; }
        bool IsOpened { get; }
        IProject Project { get; }
        ILanguageServiceProvider LanguageServices { get; }
        SourceCodeKind SourceCodeKind { get; }

        string DisplayName { get; }
        IList<string> Folders { get; }

        ISemanticModel GetSemanticModel(CancellationToken cancellationToken = null);
        CommonSyntaxTree GetSyntaxTree(CancellationToken cancellationToken = null);
        IText GetText(CancellationToken cancellationToken = null);

        bool IsEquivalentTo(IDocument document, CancellationToken cancellationToken = null);
        bool IsTopLevelEquivalentTo(IDocument document, CancellationToken cancellationToken = null);
        bool TryGetSemanticModel(out ISemanticModel semanticModel);
        bool TryGetSyntaxTree(out CommonSyntaxTree tree);
        bool TryGetText(out IText text);
#endif
    }
}
